import logging

from chatbot.bot import utils, conf
from chatbot.bot.domain.answer import Answer
from chatbot.bot.domain.question import Question
from chatbot.bot.domain.response import Response
from chatbot.bot.logic.logic_adapter import LogicAdapter


class ChatBot(object):
    """
    A conversational dialog chat bot.
    """

    def __init__(self, tokenizer, **kwargs):

        # tokenizer engine, one tenant have only one language tokenizer
        self.tokenizer = tokenizer

        # Function switch and fixed scripts
        self.configurations = kwargs.get('configurations', {})
        self.scripts = kwargs.get('scripts', {})

        # Tags, include tags' expressions, to filter result
        self.tags = kwargs.get('tags', ())

        # Channels for specific the entrance where the question came from
        self.channel = kwargs.get('channel', ())
        self.tenant = kwargs.get('tenant', ())
        # Logic adapters used by the chat bot
        logic_adapters = kwargs.get('logic_adapters', [
            'chatbot.logic.BestMatch'
        ])
        self.logic_adapters = []

        for adapter in logic_adapters:
            utils.validate_adapter_class(adapter, LogicAdapter)
            logic_adapter = utils.initialize_class(adapter, self, **kwargs)
            self.logic_adapters.append(logic_adapter)

        # processors used by the chat bot
        preprocessors = kwargs.get(
            'preprocessors', [
                'chatbot.bot.preprocessors.clean_whitespace'
            ]
        )
        self.preprocessors = []

        for preprocessor in preprocessors:
            self.preprocessors.append(utils.import_module(preprocessor))

        self.logger = kwargs.get('logger', logging.getLogger(__name__))

        # Allow the bot to save input it receives so that it can learn
        self.read_only = kwargs.get('read_only', False)

    def get_response(self, question=None, **kwargs):
        """
        Return the bot's response based on the input.

        :param question: An question object or string.
        :returns: A response to the input.
        :rtype: Question

        :param additional_response_selection_parameters: Parameters to pass to the
            chat bot's logic adapters to control response selection.
        :type additional_response_selection_parameters: dict

        :param persist_values_to_response: Values that should be saved to the response
            that the chat bot generates.
        :type persist_values_to_response: dict
        """

        additional_response_selection_parameters = kwargs.pop('additional_response_selection_parameters', {})

        if hasattr(question, 'text') is None or not question.text:
            raise self.ChatBotException(
                'Either a statement object or a "text" keyword '
                'argument is required. Neither was provided.'
            )

        input_question = question

        # tokenize the question.text
        self._tokenizer(input_question)

        # Preprocess the input statement
        for preprocessor in self.preprocessors:
            input_question = preprocessor(input_question)

        response = self.generate_response(input_question, additional_response_selection_parameters)

        if not self.read_only:
            self.learn_response(response)

        return response

    def generate_response(self, input_statement: Question, additional_response_selection_parameters=None) -> Response:
        """
        Return a response based on a given input statement.

        :param input_statement: The input statement to be processed.
        """
        results = []
        result = None
        max_confidence = -1

        for adapter in self.logic_adapters:
            if adapter.can_process(input_statement):

                output = adapter.process(input_statement, additional_response_selection_parameters)
                results.append(output)

                self.logger.info(
                    '{} selected "{}" as a response with a confidence of {}'.format(
                        adapter.class_name, output.answer, output.confidence
                    )
                )

                if output.confidence > max_confidence:
                    result = output
                    max_confidence = output.confidence
            else:
                self.logger.info(
                    'Not processing the statement using {}'.format(adapter.class_name)
                )

        class ResultOption:
            def __init__(self, statement, count=1):
                self.statement = statement
                self.count = count

        # If multiple adapters agree on the same statement,
        # then that statement is more likely to be the correct response
        if len(results) >= 3:
            result_options = {}
            for result_option in results:
                result_string = result_option.answer + ':' + (result_option.in_response_to or '')

                if result_string in result_options:
                    result_options[result_string].count += 1
                    if result_options[result_string].statement.confidence < result_option.confidence:
                        result_options[result_string].statement = result_option
                else:
                    result_options[result_string] = ResultOption(
                        result_option
                    )

            most_common = list(result_options.values())[0]

            for result_option in result_options.values():
                if result_option.count > most_common.count:
                    most_common = result_option

            if most_common.count > 1:
                result = most_common.statement

        if not result:
            result = self.get_unknown_answer(input_statement.text)

        response = Response(
            answer=result.answer,
            question=input_statement.text,
            persona='bot:' + self.name
        )

        return response

    def learn_response(self, response: Response, previous_statement=None):
        """
        Learn that the statement provided is a valid response.
        """

        if not previous_statement:
            previous_statement = self.get_latest_response(response.conversation)
            if previous_statement:
                previous_statement = previous_statement.text

        previous_statement_text = previous_statement

        self.logger.info('Adding "{}" as a response to "{}"'.format(
            response.question,
            previous_statement_text
        ))

        # Save the input statement
        return self.storage.create(**response.serialize())

    def get_latest_response(self, conversation):
        """
        Returns the latest response in a conversation if it exists.
        Returns None if a matching conversation cannot be found.
        """
        from chatterbot.conversation import Statement as StatementObject

        conversation_statements = list(self.storage.filter(
            conversation=conversation,
            order_by=['id']
        ))

        # Get the most recent statement in the conversation if one exists
        latest_statement = conversation_statements[-1] if conversation_statements else None

        if latest_statement:
            if latest_statement.in_response_to:

                response_statements = list(self.storage.filter(
                    conversation=conversation,
                    text=latest_statement.in_response_to,
                    order_by=['id']
                ))

                if response_statements:
                    return response_statements[-1]
                else:
                    return StatementObject(
                        text=latest_statement.in_response_to,
                        conversation=conversation
                    )
            else:
                # The case that the latest statement is not in response to another statement
                return latest_statement

        return None

    class ChatBotException(Exception):
        pass

    def _tokenizer(self, question):
        """
        tokenize statement text into statement search_text
        """

        # tokenize the query text
        seg_words_with_stopwords = self.tokenizer.get_seg_words(question.text, False)

        seg_words_without_stopwords = self.tokenizer.remove_stopwords(seg_words_with_stopwords)

        # serialize it by space and  fill into search_text and search_in_response_to
        question.seg_text = conf.SEG_SEPARATOR.join(seg_words_with_stopwords)

        seg_text_without_stopwords = conf.SEG_SEPARATOR.join(seg_words_without_stopwords)

        # add segment word and synonym word after query
        for w in seg_words_with_stopwords:
            synonym_word = self.tokenizer.get_synonym_word(w)
            if synonym_word:
                seg_text_without_stopwords += conf.SEG_SEPARATOR
                seg_text_without_stopwords += synonym_word

        question.seg_text_without_stopwords = question.text + conf.SEG_SEPARATOR + seg_text_without_stopwords

        return question

    def get_unknown_answer(self, query_text: str) -> Answer:
        unknown_scripts = self.scripts.get(conf.SCRIPT_UNKNOWN_REPLY)
        size = len(unknown_scripts)

        from random import randint
        idx = randint(0, size - 1)

        answer = Answer(text=unknown_scripts[idx])
        answer.confidence = 1

        return answer

    @property
    def language(self):
        return self.configurations.get(conf.BOT_LANGUAGE, conf.BOT_LANGUAGE_DEFAULT)

    @property
    def name(self):
        return self.channel.bot_name

    @property
    def avatar(self):
        return self.channel.bot_avatar.url

    def get_directly_reply_threshold(self):
        threshold = self.configurations.get(conf.BOT_DIRECTLY_REPLY_THRESHOLD,
                                            conf.BOT_DIRECTLY_REPLY_THRESHOLD_DEFAULT)
        if threshold:
            threshold = float(threshold)
        else:
            threshold = conf.BOT_DIRECTLY_REPLY_THRESHOLD_DEFAULT
        return threshold

    def greeting_info(self):
        return {
            conf.SCRIPT_GREETING: self.scripts.get(conf.SCRIPT_GREETING, conf.SCRIPT_GREETING_DEFAULT),
            conf.SCRIPT_GREETING_QUESTION_RECOMMEND_GUIDING: self.scripts.get(
                conf.SCRIPT_GREETING_QUESTION_RECOMMEND_GUIDING,
                conf.SCRIPT_GREETING_QUESTION_RECOMMEND_GUIDING_DEFAULT),
            conf.SCRIPT_GREETING_RECOMMEND_QUESTION: self.scripts.get(
                conf.SCRIPT_GREETING_RECOMMEND_QUESTION, ())
        }
